Skip to content

feat(files): stream uploads to disk instead of buffering#9527

Merged
mscolnick merged 2 commits into
mainfrom
ms/feature/files-create-streaming-upload
May 16, 2026
Merged

feat(files): stream uploads to disk instead of buffering#9527
mscolnick merged 2 commits into
mainfrom
ms/feature/files-create-streaming-upload

Conversation

@mscolnick

Copy link
Copy Markdown
Contributor

Previously /api/files/create read the entire UploadFile into memory
before writing to disk, peaking at ~100 MB for a 100 MB upload. Now we
drain the upload in 1 MiB chunks straight to a .part temp file and
atomically rename on success, so peak memory stays bounded regardless
of file size and a failed upload never leaves a half-written file at
the final path.

  • OSFileSystem.stream_create_file does the chunked write with atomic
    rename and cleanup on failure
  • parse_multipart_request now hands callers un-read UploadFile
    handles (instead of pre-read bytes), so streaming is possible without
    giving up the small-payload .read() path
  • Directories and the default-template notebook still go through the
    in-memory create_file_or_directory path; only real file content
    streams

Copilot AI review requested due to automatic review settings May 12, 2026 20:03
@vercel

vercel Bot commented May 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment May 16, 2026 9:49pm

Request Review

@github-actions github-actions Bot added the bash-focus Area to focus on during release bug bash label May 12, 2026
@mscolnick mscolnick requested a review from kirangadhave May 12, 2026 20:06

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="marimo/_server/files/os_file_system.py">

<violation number="1" location="marimo/_server/files/os_file_system.py:243">
P1: This re-loads the whole uploaded file into memory after streaming. Return metadata directly instead of calling `get_details()` here.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant API as API: create_file_or_directory
    participant Utils as Utils: parse_multipart_request
    participant Starlette as Starlette: UploadFile
    participant FS as FS: OSFileSystem
    participant Disk as Local Disk

    Client->>API: POST /api/files/create (Multipart)
    API->>Utils: CHANGED: parse_multipart_request()
    Utils->>Starlette: Access request form data
    Note over Utils,Starlette: CHANGED: No longer calls .read() <br/>to buffer full content into memory
    Utils-->>API: Return MultipartRequest (contains UploadFile handles)

    alt NEW: Streaming Path (Type is "file" or "notebook")
        API->>FS: NEW: stream_create_file(path, name, upload_file)
        FS->>FS: _validate_create_name()
        FS->>Disk: Create unique temporary file (path/name.part)
        
        loop Every 1 MiB Chunk
            FS->>Starlette: CHANGED: read(1024 * 1024)
            Starlette-->>FS: bytes
            FS->>Disk: write chunk to .part file
        end

        alt Success
            FS->>Disk: NEW: Atomic rename (.part to final name)
            FS->>FS: get_details(final_path)
            FS-->>API: Return FileInfo
        else Failure (e.g. Disconnect/IO Error)
            FS->>Disk: NEW: unlink/cleanup .part file
            FS-->>API: Raise Exception
        end

    else Traditional Path (Directory or Template)
        API->>FS: create_file_or_directory(..., contents=None)
        FS->>Disk: mkdir or write default template
        FS-->>API: Return FileInfo
    end

    API-->>Client: 200 OK (FileCreateResponse)
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread marimo/_server/files/os_file_system.py Outdated
@mscolnick mscolnick force-pushed the ms/feature/files-create-streaming-upload branch from 977f8ec to 2612cf9 Compare May 12, 2026 20:06
@mscolnick mscolnick added the enhancement New feature or request label May 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves file upload handling for POST /api/files/create by enabling streaming writes to disk (instead of buffering entire uploads in memory), and updates multipart parsing to return un-read UploadFile handles so callers can stream.

Changes:

  • Added OSFileSystem.stream_create_file() to drain an async byte source into a .part temp file and atomically rename on success.
  • Updated parse_multipart_request() to return raw UploadFile objects (not pre-read bytes) and adjusted the /api/files/create endpoint to stream uploads.
  • Updated tests and OpenAPI docs to reflect the new multipart/file-upload behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/_server/files/test_os_file_system.py Adds async tests covering chunked streaming, unique names, traversal rejection, and cleanup on failure.
tests/_server/api/test_api_utils.py Updates multipart parsing test to expect UploadFile handles and reads bytes in-endpoint.
marimo/_server/files/os_file_system.py Introduces streaming upload write path + shared name validation.
marimo/_server/api/utils.py Changes multipart parsing to return unconsumed UploadFile objects for streaming.
marimo/_server/api/endpoints/file_explorer.py Switches /create to stream uploads when provided.
marimo/_server/models/files.py Clarifies FileCreateMultipartRequest is schema-only and explains runtime behavior.
packages/openapi/api.yaml Regenerates/adjusts OpenAPI descriptions (including multipart schema clarification).
packages/openapi/src/api.ts Regenerates/adjusts TS OpenAPI typings/doc comments for multipart clarification/formatting.

Comment thread marimo/_server/files/os_file_system.py Outdated
Comment thread marimo/_server/api/endpoints/file_explorer.py Outdated
Comment thread marimo/_server/api/utils.py Outdated
Comment thread marimo/_server/files/os_file_system.py
Comment thread marimo/_server/api/endpoints/file_explorer.py
Comment thread marimo/_server/files/os_file_system.py Outdated
self._validate_create_name(name)

full_path = Path(path) / name
full_path = _generate_unique_path(full_path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have a toctou race here. We are getting the file path, then do a bunch of work and the actually write on line 257 with replace. if some other concurrent upload happens meanwhile and picks up the same name we might have overwrites.

Previously /api/files/create read the entire UploadFile into memory
before writing to disk, peaking at ~100 MB for a 100 MB upload. Now we
drain the upload in 1 MiB chunks straight to a `.part` temp file and
atomically rename on success, so peak memory stays bounded regardless
of file size and a failed upload never leaves a half-written file at
the final path.

- `OSFileSystem.stream_create_file` does the chunked write with atomic
  rename and cleanup on failure
- `parse_multipart_request` now hands callers un-read `UploadFile`
  handles (instead of pre-read bytes), so streaming is possible without
  giving up the small-payload `.read()` path
- Directories and the default-template notebook still go through the
  in-memory `create_file_or_directory` path; only real file content
  streams
@codecov

codecov Bot commented May 16, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 23.55kB (0.09%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
marimo-esm 25.16MB 23.55kB (0.09%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: marimo-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/cells-*.js 2.08kB 706.77kB 0.29%
assets/index-*.js 834 bytes 607.56kB 0.14%
assets/index-*.css -16 bytes 366.01kB -0.0%
assets/JsonOutput-*.js 124 bytes 360.96kB 0.03%
assets/edit-*.js 324 bytes 325.09kB 0.1%
assets/add-*.js 9.24kB 201.99kB 4.79%
assets/layout-*.js 10 bytes 198.24kB 0.01%
assets/cell-*.js 996 bytes 184.14kB 0.54%
assets/file-*.js 69 bytes 46.84kB 0.15%
assets/file-*.js 32 bytes 102.67kB 0.03%
assets/worker-*.js 140 bytes 85.23kB 0.16%
assets/save-*.js 155 bytes 81.24kB 0.19%
assets/panels-*.js 1.47kB 47.21kB 3.22%
assets/chat-*.js 568 bytes 15.22kB 3.88%
assets/chat-*.js 5.97kB 14.66kB 68.7% ⚠️
assets/chat-*.js 240 bytes 32.63kB 0.74%
assets/useNotebookActions-*.js 29 bytes 27.35kB 0.11%
assets/react-*.browser.esm-BdtIs0E-.js (New) 25.64kB 25.64kB 100.0% 🚀
assets/session-*.js 1 bytes 25.04kB 0.0%
assets/state-*.js 13 bytes 24.8kB 0.05%
assets/vega-*.browser-C8wT63Va.js (New) 24.8kB 24.8kB 100.0% 🚀
assets/home-*.js 41 bytes 24.03kB 0.17%
assets/useEventListener-*.js 60 bytes 12.84kB 0.47%
assets/vega-*.js 82 bytes 12.79kB 0.65%
assets/CellStatus-*.js -133 bytes 11.22kB -1.17%
assets/run-*.js 58 bytes 9.83kB 0.59%
assets/react-*.esm-BNzu6e7h.js (New) 8.37kB 8.37kB 100.0% 🚀
assets/ws-*.js 86 bytes 7.52kB 1.16%
assets/emotion-*.esm-C59xfSYt.js (New) 4.37kB 4.37kB 100.0% 🚀
assets/mermaid-*.core-CMygPhv_.js (New) 2.38kB 2.38kB 100.0% 🚀
assets/semaphore-*.js (New) 788 bytes 788 bytes 100.0% 🚀
assets/maps-*.js -198 bytes 595 bytes -24.97%
assets/fileToBase64-*.js 52 bytes 531 bytes 10.86% ⚠️
assets/external-*.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/ban-*.js (New) 181 bytes 181 bytes 100.0% 🚀
assets/react-*.browser.esm-BUNcfKXO.js (Deleted) -25.64kB 0 bytes -100.0% 🗑️
assets/vega-*.browser-BegSZk0G.js (Deleted) -24.8kB 0 bytes -100.0% 🗑️
assets/react-*.esm-D9xfKaPZ.js (Deleted) -8.37kB 0 bytes -100.0% 🗑️
assets/emotion-*.esm-DD4AwVTU.js (Deleted) -4.37kB 0 bytes -100.0% 🗑️
assets/mermaid-*.core-BQULBKwL.js (Deleted) -2.38kB 0 bytes -100.0% 🗑️

@kirangadhave kirangadhave left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@mscolnick mscolnick merged commit 865d65f into main May 16, 2026
49 checks passed
@mscolnick mscolnick deleted the ms/feature/files-create-streaming-upload branch May 16, 2026 22:02
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.23.7-dev31

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bash-focus Area to focus on during release bug bash enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants